﻿//
// Code for the FlatFileWriter is from this thread:
// http://forums.asp.net/t/1338674.aspx/1/10
// It performs very well, so I felt no need to write my own.
//
// Only modification is I changed the constructor from accepting a Stream to a StreamWriter
// then I removed the Finalize and Dispose method closing the Stream as it'd now be the
// responsbility of the calling code.
//


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Linq.Expressions;
using System.IO;
using System.Text;

namespace TRL.TRLAdmin
{
    /// <summary>
    /// Provides a writer for "flat file" formatted data where fields are separated by a particular
    /// character and records by a string (usually NewLine or Carriage Return + NewLine; use any
    /// one- or two-character separator).
    /// Use of FlatFileWriter and the corresponding FlatFileReader class allows abstracting the
    /// details of the file and working with fields and records. Fields may contain the field or record
    /// separator or both; the reader/writer takes care of the representation.
    /// </summary>
    public class FlatFileWriter : IDisposable
    {
        /// <summary>
        /// Initializes a new instance of the FlatFileWriter class.
        /// </summary>
        /// <param name="baseStream">Stream to write to.</param>
        /// <param name="fieldSeparator">Character used to separate fields.</param>
        /// <param name="escapeChar">Character used to escape field values.</param>
        /// <param name="recordEnd">
        /// Record separator. Must be one or two characters long.
        /// </param>
        public FlatFileWriter(StreamWriter baseStream, char fieldSeparator, char escapeChar, string recordEnd)
        {
            if (baseStream == null) throw new ArgumentNullException("baseStream");
            if (recordEnd.Length == 0 || recordEnd.Length > 2)
                throw new ArgumentException("Parameter recordEnd must be one or two characters long.");

            w = baseStream;
            this.recordEnd = recordEnd;
            this.fieldSeparator = fieldSeparator;
            this.escapeChar = escapeChar;
        }

        /// <summary>
        /// Writes a field to the base stream.
        /// </summary>
        /// <param name="value">Field value.</param>
        public void WriteField(string value)
        {
            bool needEscape;
            value = formatField(value, out needEscape);
            if (!atRecordStart) w.Write(fieldSeparator);
            if (needEscape) w.Write(escapeChar);
            w.Write(value);
            if (needEscape) w.Write(escapeChar);
            atRecordStart = false;
        }

        /// <summary> 
        /// Closes the writer. 
        /// </summary> 
        public void Close()
        {
            w.Close();
        }

        /// <summary> 
        /// Disposes the writer. 
        /// </summary> 
        public void Dispose()
        {
            w.Dispose();
        } 

        /// <summary>
        /// Writes a record with the given field values to the base stream.
        /// </summary>
        /// <param name="record">Array of field values.</param>
        public void WriteRecord(string[] record)
        {
            foreach (string f in record) WriteField(f);
            WriteRecordEnd();
        }

        /// <summary>
        /// Writes the record separator to the base stream.
        /// </summary>
        public void WriteRecordEnd()
        {
            w.Write(recordEnd);
            atRecordStart = true;
        }

        // ---- END PUBLIC INTERFACE ----- //


        bool atRecordStart = true;
        char fieldSeparator;
        char escapeChar;
        string recordEnd;
        StreamWriter w;
        StringBuilder sb = new StringBuilder();
       



        /// <summary>
        /// Checks if a field value requires escaping when written in "flat file format", and aditionally
        /// formats the field if necessary.
        /// </summary>
        /// <param name="s">Field value, as-is.</param>
        /// <param name="needEscape">Whether or not escape characters must be written for the field.</param>
        /// <returns>The "flat file" representation of the field value, minus any escape characters.</returns>
        string formatField(string s, out bool needEscape)
        {
            needEscape = false;
            if (s == "") return s;


            bool containsFieldDelimiter = false;
            bool containsRecordDelimiter = false;
            int escapeCharCount = 0;
            char recordEndStartSymbol = recordEnd[0];
            int count = s.Length;


            for (int i = 0; i < count; i++)
            {
                char c = s[i];
                if (!containsFieldDelimiter) containsFieldDelimiter = (c == fieldSeparator);
                if (c == escapeChar) ++escapeCharCount;
                if (!containsRecordDelimiter && c == recordEndStartSymbol)
                {
                    containsRecordDelimiter = (recordEnd.Length == 1 || (i < count - 1 && s[i + 1] == recordEnd[1])); 
                }
            }


            needEscape = (containsFieldDelimiter || (escapeCharCount > 0) || containsRecordDelimiter);


            if (escapeCharCount > 0)
            {
                sb.Length = 0;
                for (int i = 0; i < s.Length; i++)
                {
                    sb.Append(s[i]);
                    if (s[i] == escapeChar) sb.Append(escapeChar);
                }
                s = sb.ToString();


                #region Implementation note
                /*
                The stringbuilder is being reused between calls to this method to minimize
                memory allocations and thus optimize speed. Therefore it is an instance member,
                with the same lifetime as the writer that contains it. In the case that a very
                big field is held in it, the stringbuilder will reallocate and it could potentially
                become quite large. After the field has been written we do not need to hold on
                to this larger capacity, so we truncate the reader and reallocate 256 characters.
*/
                #endregion
                if (sb.Capacity != 256) { sb.Length = 0; sb.Capacity = 256; }
            }


            return s;
        }
    }
}